Codetopia 創城初期,百廢待舉。一份「城市景觀色系綱要」的政策文件,需要下發到各個局處。
📜 文件內容: 「為統一市容,所有新建公共設施,主色調採 #333 現代灰。」
沒想到,意外發生了...
工務局收到的 email 副本寫著「現代灰」,但公園管理處的職員因為開會沒聽到,憑記憶寫了份筆記,變成了「復古金」。結果,城東的公車站是冷冽的灰色,城西的公園長椅卻是金光閃閃。市民們都看傻了,Codetopia 的美感瞬間變成一場災難!😱
這個混亂的根源是什麼?資訊來源不唯一。每個單位都有自己的一份「政策副本」,導致資訊不同步,最終城市建設亂了套。我們需要一個絕對的、唯一的、所有人都認可的資訊來源——市長辦公室。
🧭 術語卡 (今日導覽)
- GoF (Gang of Four):設計模式的經典著作,本文將其視為物件之間協作的「微觀」結構。
- SSOT (Single Source of Truth):唯一真實來源。確保所有決策都基於同一份、無歧義的資訊,是 Singleton 模式要解決的核心問題之一。
- DI (Dependency Injection):依賴注入。一種重要的解耦技術,將一個物件所依賴的外部服務(如市長辦公室)透過參數「注入」,而不是讓物件自己去尋找。
在還沒有建立「市長辦公室」這個概念前,我們的程式碼裡可能充斥著這樣的「違章建築」:
Global Variables
) 📜: 在程式碼的某個角落宣告一個全域變數 CITY_CONFIG
。它看似方便,誰都能讀取,但誰也都能修改!今天工務局把它改成灰色,明天公園處就能改成金色,完全失控,是 bug 的溫床。Props Drilling
) 🏃♂️: 為了確保資訊一致,我們只好把一個巨大的 config
物件,從市長一路傳遞給局長,局長再傳給處長,處長再傳給科員... 即使最底層的科員只需要其中一個小設定,也得接收整份文件。這造成了荒謬的資訊冗餘和緊密的依賴鏈。Inconsistent Instantiation
) 👨💼👨💼: 每個局處都自己 new
一個「政策顧問」物件。結果城市裡有 A 顧問、B 顧問、C 顧問,他們拿到的可能是不同時間點的政策版本,給出了完全矛盾的建議。Service Locator
) 🕵️: 一個看似方便的全域「服務查詢站」,任何物件都可以從中獲取 Singleton 實例。這雖然比裸的全域變數好一點,但本質上還是隱藏了依賴關係,讓測試和維護變得困難。讓我們用一小段 Python 程式碼,精準重現開頭那場「市容顏色災難」。這段程式碼聞起來就有一股濃濃的『違章建築』味,而且,它注定會驗收失敗。
# 違章建築:每個局處都自己 new 一個「政策辦公室」
class PolicyOffice: # 注意:這還不是我們的 Singleton 市長辦公室
def __init__(self):
self.color_policy = None
def set_policy(self, color):
self.color_policy = color
def get_policy(self):
return self.color_policy
def public_works_dept_action():
office = PolicyOffice()
office.set_policy("#2E7D32") # 工務局根據自己的理解設定了綠色系
return office.get_policy()
def parks_dept_action():
office = PolicyOffice()
office.set_policy("#1E88E5") # 公園處則設定了藍色系
return office.get_policy()
# 驗收測試:這段測試注定會失敗!
print("--- 災難現場重現 ---")
pw_color = public_works_dept_action()
p_color = parks_dept_action()
try:
assert pw_color == p_color
print("✅ 咦?居然統一了?這不科學...")
except AssertionError:
print(f"❌ 驗收失敗!市容分裂:工務局是 {pw_color},公園處是 {p_color}。")
看到那個 AssertionError
了嗎?這就是多頭馬車的直接後果。為了解決這個問題,我們需要引入今天的工法。
Singleton 模式,就是 Codetopia 的市長辦公室設立法案。它是一種創生型模式,其核心思想極度專一:
確保一個類別只有一個實例,並提供一個全域的存取點來獲取這個實例。
就像 Codetopia 不能有兩個市長一樣,某些物件,我們也必須確保它們在記憶體中是獨一無二的。
在動工前,先問問自己:我只是需要一個掛外套的衣帽架,還是真的需要一棟市政府大樓?
config.py
檔案中定義變數和函式,任何地方 import config
都會存取到同一個實例,這是最 Pythonic 且簡單的方式。MayorOffice.get_instance()
,不如在局處成立時,由外部(例如一個中央的 AppBuilder
)把市長辦公室的實例傳遞進去。這樣依賴關係一目了然,且在測試時可以輕鬆換成一個「代理市長」(Mock Object)。市長什麼時候上班?這決定了我們的 Singleton 類型。
類型 | 初始化時機 | 優點 | 缺點 |
---|---|---|---|
懶漢式 (Lazy) | 第一次呼叫 get_instance() 時 |
節省啟動時間與記憶體 | 首次呼叫較慢,多執行緒需處理同步問題 |
餓漢式 (Eager) | 程式啟動時 (類別載入時) | 執行緒安全,立即可用 | 拖慢啟動速度,即使完全沒用到也會佔用資源 |
視角 | 觀念/模式 | 在 Codetopia 的說法 |
---|---|---|
微觀 (GoF) | Singleton (唯一實例+全域存取點) | 市長辦公室 / 設定中心 |
中觀 (訊息/事件) | 單一配置/目錄服務 (Actor: Registry) | 城市設定服務 (Config Server) |
宏觀 (MAS) | DF (Directory Facilitator) 或治理代理 | 城市黃頁服務 (Directory Service) |
這張圖的目的是將同一個「唯一權威」的概念,在三個不同的視角下對齊,幫助我們從單純的類別結構,思考到系統內的資訊流動與角色協作。
這張藍圖定義了市長辦公室的結構:它自己創建自己,並封鎖了其他人隨意建造的入口。
各局處(Client)如何向唯一的設定中心(Singleton)取得一致的資訊。
在 Python 中,最穩固的 Singleton 施工法是利用 __new__
,它在 __init__
之前被呼叫,是真正控制物件創建的地方。
import threading
class MayorOffice:
_instance = None
_lock = threading.Lock()
# __new__ 才是真正創建實例的地方
def __new__(cls, *args, **kwargs):
if not cls._instance:
with cls._lock:
# Double-Checked Locking to ensure thread safety
if not cls._instance:
cls._instance = super().__new__(cls)
return cls._instance
# __init__ 負責初始化,但要防止重複執行昂貴操作
def __init__(self):
# 使用一個旗標來確保初始化邏輯只執行一次
if getattr(self, "_initialized", False):
return
self._policies = {}
self.city_motto = "Build with passion, code with purpose."
self._initialized = True
print(f"市長辦公室 (ID: {id(self)}) 已建立並初始化。")
# --- 政策管理 ---
def set_policy(self, key, value):
self._policies[key] = value
def get_policy(self, key, default=None):
return self._policies.get(key, default)
@staticmethod
def get_instance():
"""提供一個熟悉的靜態方法來獲取實例,這在本質上是 __new__ 的一個包裝"""
return MayorOffice()
程式碼說明:
_instance
是一個類別變數,用來存放那個唯一的實例。__new__
方法攔截了物件的創建過程。只有在 _instance
為 None
時,才會真正創建一個新物件。_lock
和雙重檢查鎖確保了在多執行緒的混亂施工現場,也只會有一位市長誕生。__init__
中的 _initialized
旗標確保了即使多次拿到同一個實例,昂貴的初始化設定(如讀取設定檔)也只會執行一次。現在,我們有了 Singleton 這個強大的工法。讓我們回到最初的災難現場,用市長辦公室來發布統一政策,並執行我們一開始就寫好的驗收測試,看看它是否能神奇地『由紅轉綠』!
# 首先,我們讓各局處的行為改成向唯一的市長辦公室查詢
# --- 模擬城市各局處的運作 ---
def public_works_dept_task():
mayor = MayorOffice.get_instance()
color = mayor.get_policy("landscape_color")
print(f"工務局收到政策,景觀色為: {color} (辦公室 ID: {id(mayor)})")
return color
def parks_dept_task():
mayor = MayorOffice.get_instance() # 同樣的 get_instance() 呼叫
color = mayor.get_policy("landscape_color")
print(f"公園處收到政策,景觀色為: {color} (辦公室 ID: {id(mayor)})")
return color
# --- 執行與驗收 ---
print("\n--- 用 Singleton 工法進行修復與驗收 ---")
# 1. 市長發布統一命令
MayorOffice.get_instance().set_policy("landscape_color", "#333-Modern-Gray")
# 2. 執行我們在「違章建築」章節中,那個注定會失敗的驗收流程
pw_color = public_works_dept_task()
p_color = parks_dept_task()
try:
assert pw_color == p_color
print(f"\n✅ 驗收通過!市容終於統一為 {pw_color}。")
except AssertionError:
print(f"\n❌ 驗收失敗!市容依然分裂:工務局是 {pw_color},公園處是 {p_color}。")
劇情收束:看到沒?我們完全沒有修改驗收邏輯,僅僅是改變了局處獲取資訊的方式(從 new PolicyOffice()
改為 MayorOffice.get_instance()
),原先失敗的測試就通過了!這就是 Singleton 作為「唯一真實來源 (SSOT)」的威力。一場城市美學危機,就此化解。
今天我們在「微觀」層面談論了 Singleton 物件。當我們將視角拉高:
_policies
),它就成了一個隱性的全域狀態機,非常容易在複雜系統中引發難以追蹤的 bug。一個更好的實踐是,讓 Singleton 提供的設定是不可變的 (immutable),任何變更都應該是生成一個新版本的設定,而不是原地修改。直接測試依賴 Singleton 的程式碼是個噩夢。有可能出現一種情況,當測試工務局時,市長的政策被改成了 "Holiday-Red";下一個測試公園處的案例,可能會因為讀到這個被污染的狀態而失敗。
解決方案:
setup
和 teardown
機制,在每個測試案例結束後,強制重置 Singleton 狀態。# 僅供測試環境使用!
def _reset_mayor_singleton_for_tests():
MayorOffice._instance = None
MayorOffice._initialized = False
parks_dept_task
自己去呼叫 MayorOffice.get_instance()
,而是讓它接收一個 mayor
物件作為參數。這樣在測試時,你可以輕鬆傳入一個假的「代理市長」(MockMayor) 來完全控制測試情境。public_works_dept_task
和 parks_dept_task
函式重構成依賴注入的形式 def task(mayor_instance): ...
。你認為這樣做之後,編寫測試會有哪些具體的好處?MayorOffice
改寫成「餓漢式」版本。提示:可以在類別定義時就直接創建 _instance
。權力愈大,耦合愈重;慎用唯一,保持彈性。
今天,我們為 Codetopia 確立了唯一的權力核心,解決了政令不一的混亂。但我們也看到了 Singleton 這把雙面刃的鋒利之處。
明天,市長辦公室要開始正式運作了,第一個任務就是為市民核發身份證件。面對五花八門的證件申請,櫃檯人員該如何高效處理,而不用寫出成噸的 if-else
呢?敬請期待 Day 3:市民服務櫃檯的秘密——Factory Method!